package ks.client.game.wordsteal;


import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;

import ks.client.interfaces.IGameInterface;

import wordsteal.entities.Board;
import wordsteal.entities.Cell;
import wordsteal.entities.CellColor;
import wordsteal.entities.GameState;
import wordsteal.entities.ReqTilesPerTurn;
import wordsteal.entities.Tile;
import wordsteal.interfaces.IWordstealApp;
import wordsteal.util.BoardLocation;


/**
 * Submit Word Controller is launched by mainframe when the submit button is pressed
 * this controller retrieves the tiles placed on the board and validates them.  It then
 * updates the appropriate entities and passes on the turn to the next player.
 * 
 * @author Dan
 */
public class SubmitWordController extends CommonMoveController {
	
	/** 
	 * Construct controller
	 * @param mainFrame MainFrame allows ability to set and retrieve entity information
	 */
	public SubmitWordController(IWordstealApp mainFrame) {
		super(mainFrame);
	}
	
	/**
	 * Assumes that the tiles have already been "dragged" onto the board and 
	 * now we are only awaiting the request to this controller.
	 * 
	 * @param callback 
	 */
	public boolean process(WordstealGameInformation info, IGameInterface callback){		
		this.callback = callback;
		boolean rc = true;
		
		// Holds the list of word strings created on the board
		ArrayList<String> words = new ArrayList<String>();
		// Holds the list of cells where new words are located
		ArrayList<ArrayList<Cell>> wordCells = new ArrayList<ArrayList<Cell>>(); 
    	// Holds the cells containing tiles played this turn
    	Hashtable<BoardLocation, Cell> playedTileCells = this.mf.getGame().getBoard().getNewTiles();
    	
		// Check if no tiles were played this turn
		if (playedTileCells.size() == 0) {
			
			rc = failTurn("No tiles have been placed!");
			
		} else if(playedTileCells.size() < 2) {
			
			rc = failTurn("You must place at least 2 tiles!");
			
		} else if (this.mf.getGame().isFirstTurn() &&
				   !isTilesOnColorCell(playedTileCells, CellColor.Green)){
			
			rc = failTurn("You must place the first word on the green square!");
			
		} else if (playedTileCells.size() != 2 && 
				   this.mf.getGame().getReqTilesPerTurn() == ReqTilesPerTurn.Two) {
			
			rc = failTurn("You must place exactly 2 tiles!");
			
		} else if (playedTileCells.size() != 3 && 
				   this.mf.getGame().getReqTilesPerTurn() == ReqTilesPerTurn.Three) {
			
			rc = failTurn("You must place exactly 3 tiles!");
			
		} else if(!this.mf.getGame().isFirstTurn() &&
				  !isTilesTouchingExistingWord(playedTileCells)) {
			
			rc = failTurn("You must place a word adjacent to one on the board!");
			
		} else if(!isTilesStraight(playedTileCells)) {
			
			rc = failTurn("Tiles must be placed in a straight line!");
			
		} else if(!isTilesContiguous(playedTileCells)) {
			
			rc = failTurn("Tiles must contiguous!");
			
		} else if(isTilesOnColorCell(playedTileCells, CellColor.Pink) && 
				   this.mf.getGame().isNoPinkEnabled()) {
			
			rc = failTurn("You cannot place on pink cells!");
			
		} else {
			Board board = mf.getGame().getBoard();
			wordCells = board.extractAdjacentWordCells(playedTileCells);
			words = board.formWordsFromCells(wordCells);
			
			String invalidWords;
			
			if (this.mf.getGame().isNoSEnabled() &&
				isViolatingNoS(wordCells)) {
				rc = failTurn("No S: You cannot extend a word by just adding S");
			} else if(!(invalidWords = findInvalidWords(words)).isEmpty()) {
				rc = failTurn("Invalid word: " + invalidWords);
			} else {
				
				// Validation is complete, go to finishSubmitWord to perform exit actions
				String playerID = info.getCurrentUser();
				boolean complete = finishSubmitWord(info.fullName(playerID), words, wordCells, playedTileCells);
				
				// in both cases, send off the info
				Properties  playerScores = WordstealGameInformation.extractScores(this.mf.getGame().getPlayers());
				
				// form move from playedTileCells
				StringBuilder move = new StringBuilder();
				for (BoardLocation bl : playedTileCells.keySet()) {
					Cell c = playedTileCells.get(bl);
					Tile t = c.getTile();
					boolean addSeparator = true;
					if (move.length() == 0) {
						addSeparator = false;
					} 
					if (addSeparator) { move.append(','); }
					//move.append(bl.toString());
					move.append("(").append(bl.row).append("/").append(bl.col).append(")");
					move.append("=");
					move.append(t.letter);
				}
				
				callback.turn(mf.getTableNumber(), playerScores, move.toString(), complete);
			}
		}

		// bad move auto skips player...
		if (!rc) {
			callback.skip(mf.getTableNumber());

			// must remember to lock GUI
			this.mf.lockUI(true);
		}
		
		// update everything.
		this.mf.updateGUI();

		// disable buttons.
		info.enableButtons(false);
		info.resetTimer();		
		return rc;
	}
	

	/**
	 * Finds all invalid words in the list
	 * @param words List of words
	 * @return Comma separated list of invalid words
	 */
	String findInvalidWords(ArrayList<String> words) {
		
		String invalidWords = "";
		
		for(Iterator<String> iter = words.iterator(); iter.hasNext();) {
			
			String word = iter.next();
			
			if(!this.mf.getGame().getDictionary().isValidWord(word.toLowerCase())) {
				
				invalidWords += word + ", ";
			}
		}
		
		if(invalidWords.isEmpty()) {
			
			return invalidWords;
			
		} else {
			
			return invalidWords.substring(0, invalidWords.length() - 2);
		}
	}
	
	/**
	 * Ends the turn due to illegal play
	 * @param status Message to display to users
	 */
	boolean failTurn(String status) {
		
		this.mf.setStatus(status);
		
		//Return tiles to rack
		this.mf.getGame().returnPlayedTiles();
		
		//Update game log
		GameState gs = new GameState(this.mf.getGame(), status, 0, null);
		this.mf.getGame().getGameLog().pushGameState(gs);
		
		//The player loses their turn since their word
		//submission failed
		//this.mf.getGame().nextTurn();
		this.mf.getGame().setConsecSkips(this.mf.getGame().getConsecSkips() + 1);
		return false;
	}


	/**
	 * isTilesTouchingExistingWord verifies that at least one of the newly placed tiles is touching
	 * a tile on the board that already has an owner
	 * @param placedTileCells containing the newly placed tiles on the board
	 * @return Whether the tiles are touching an existing word
	 */
	boolean isTilesTouchingExistingWord(Hashtable<BoardLocation, Cell> placedTileCells){

	    Iterator<BoardLocation> itr = placedTileCells.keySet().iterator();	    
	    Board board = this.mf.getGame().getBoard();
	    
	    // Loop through the cells, checking the adjacent cell in each direction to see if
	    // a tile with an owner is present
	    while (itr.hasNext()) {
	    	
	    	BoardLocation bl = itr.next();
	    	Cell[] adjacentCells = new Cell[] {
	    			board.getCell(bl.row, bl.col + 1),
	    			board.getCell(bl.row, bl.col - 1),
	    			board.getCell(bl.row + 1, bl.col),
	    			board.getCell(bl.row - 1, bl.col)
	    	};
	    	
	    	for(int i = 0; i < adjacentCells.length; i++) {
	    		
	    		if (adjacentCells[i] != null &&
	    			adjacentCells[i].getTile() != null &&
	    			adjacentCells[i].getTile().getOwner() != null) {
	    	    				
	    	    		return true;
	    	    }
	    	}
	    }
	    
	    return false;
	}
	
	/**
	 * isTilesStraight verifies that the tiles placed on the board have been placed in a straight line either
	 * vertically or horizontally
	 * @param placedTileCells Cells containing the tiles placed by the player this turn
	 * @return Whether the tiles are in a straight line
	 */
	boolean isTilesStraight(Hashtable<BoardLocation, Cell> placedTileCells){
		
		// We must first check if the first two tile in the newly placed word
		// are in the same row or in the same column (or neither). After we
		// have established their direction, we must make sure the remaining
		// tiles are in the same row/column. LoopState controls what phase
		// of this algorithm we are in.
		//
		// 0 - We have not yet determined if the word is horizontal or vertical
		// 1 - The word is horizontal, we must check if the tiles are in the same row
		// 2 - The word is vertical, we must check if the tiles are in the same column
		int loopState = 0;
		
		Iterator<BoardLocation> itr = placedTileCells.keySet().iterator();
		
		// Stores the previous tile, as we are checking inductively whether each tile
		// is in the same row/column as its neighbor
		BoardLocation previousBoardLocation = itr.next();
		
		// Loop through the cells
		while (itr.hasNext()) {
    		
			BoardLocation currentBoardLocation = itr.next();
			
			// We have not yet determined if the word is horizontal or vertical
			if(loopState == 0) {
				
				// If the tiles are on the same row, the word is horizontal
				if(currentBoardLocation.row == previousBoardLocation.row) {
					// Set state to horizontal
					loopState = 1;
					
				} else if(currentBoardLocation.col == previousBoardLocation.col) {
					// If the tiles have the same column, set the loop state to vertical
					loopState = 2;
					
				} else {
					// If the first two tiles share neither, then the word is not straight
					return false;
				}
			} else if(loopState == 1) {
				// The word is horizontal, so we must check if the next tile is on the same row
				if(currentBoardLocation.row != previousBoardLocation.row) {
					
					return false;
				}
			} else if(loopState == 2) {
				// The word is vertical, so we must check if the next tile is on the same column
				if(currentBoardLocation.col != previousBoardLocation.col) {
					
					return false;
				}
			}
			// Update the previous tile
			previousBoardLocation = currentBoardLocation;
		}
		
		// If the algorithm fails to find a tile that is out of line, or if the word only
		// has one tile, return true.
		return true;
	}
	
	/**
	 * isTilesContiguous verifies that there are no gaps in the newly placed word, it checks both
	 * newly placed tiles and current tiles on the board
	 * @param placedTileCells
	 * @return Whether or not tiles are contiguous
	 */
	boolean isTilesContiguous(Hashtable<BoardLocation, Cell> placedTileCells) {
		
		// The algorithm works thusly: an arbitrary cell is selected from the list of cells on
		// which the player placed tiles this turn (these cells are assumed to be in a straight
		// line). The algorithm searches the board in each direction from the cell, adding to
		// tileCount whenever it encounters a tile placed by the player this turn and stopping
		// whenever it encounters an empty cell or the end of the board. It then compares tileCount
		// to the number of tiles the player placed this turn. If the player placed a contiguous
		// word, the algorithm should have been able to reach all of the cells he or she placed a
		// tile in by searching outward from the arbitrary cell.
		int tileCount = 0;
		
	    Iterator<BoardLocation> itr = placedTileCells.keySet().iterator();
	    
	    // Grab an arbitrary cell's location, and add it to the tileCount
	    BoardLocation bl = itr.next(); 
		tileCount++;
		
		Board board = this.mf.getGame().getBoard();
		
		// Check in the eastern direction
		for (int i = bl.col + 1; i < Board.boardStructure.length; i++ ) {
			
			Tile tile = board.getCell(bl.row, i).getTile();
			
    		if (tile != null) {
    			
    			if(tile.getOwner() == null) {
				
					tileCount++;
    			}	
			} else {
				
				break;
			}	
		}
		
		// Check in the western direction
		for (int i = bl.col - 1; i >= 0; i-- ) {
			
			Tile tile = board.getCell(bl.row, i).getTile();
			
    		if (tile != null) {
    			
    			if(tile.getOwner() == null) {
				
					tileCount++;
    			}	
			} else {
				
				break;
			}	
		}
		
		// Check in the southern direction
		for (int i = bl.row + 1; i < Board.boardStructure.length; i++ ) {
			
			Tile tile = board.getCell(i, bl.col).getTile();
			
    		if (tile != null) {
    			
    			if(tile.getOwner() == null) {
				
					tileCount++;
    			}	
			} else {
				
				break;
			}	
		}
		
		// Check in the northern direction
		for (int i = bl.row - 1; i >= 0; i-- ) {
			
			Tile tile = board.getCell(i, bl.col).getTile();
			
    		if (tile != null) {
    			
    			if(tile.getOwner() == null) {
				
					tileCount++;
    			}	
			} else {
				
				break;
			}	
		}
    
		// Verify all tiles were counted
    	if (tileCount < placedTileCells.size()){
    		
    		return false;
    		
    	} else {
    	
    		return true;
    	}
	}	


	/**
	 * Checks whether or not the play violates the No S rule
	 * @param wordCells Cells containing potential words
	 * @return Whether or not the play violates the No S rule
	 */
	boolean isViolatingNoS(ArrayList<ArrayList<Cell>> wordCells) {
		
		// We loop through each potential word and check if:
		// 		1. It is greater than 2 letters
		//		2. Its last tile is an "S"
		//		3. If so, if it is a tile placed this turn
		//		4. If so, if the tile before the last was NOT placed this turn
		//			(otherwise, the player appended more than just "S")
		Iterator<ArrayList<Cell>> iter = wordCells.iterator();
		
		while(iter.hasNext()) {
			
			ArrayList<Cell> word = iter.next();
			
			// Word has to be greater than 2 letters (adding "S" to "A" to form "AS" is legal
			if(word.size() > 2) {
				
				Tile lastTile = word.get(word.size() - 1).getTile();
				
				// If the last tile is S and newly placed...
				if(lastTile.letter.equals("S") &&
				   lastTile.getOwner() == null) {
					
					// Check if the rest of the word is old
					boolean isRestOfWordOld = true;
					
					for(int i = 0; i < word.size() - 1; i++) {
					
						if(word.get(i).getTile().getOwner() == null) {
							
							isRestOfWordOld = false;
							break;
						}
					}
					
					if(isRestOfWordOld) {
						return true;
					}
				}
			}
		}
		
		return false;
	}
	


}
			
